require 'net/smtp'

module BulkMail
  # Sender instances perform the actual work of sending an email. Each sender 
  # sets up a thread pool (by default with up to 20 threads) for performing the
  # work. When creating a Sender instance, a hash of options is supplied:
  # 
  #   BulkMail::Sender.new({
  #     :list => addresses,
  #     :from => my_address,
  #     :helo => my_helo_domain,
  #     :concurrent => 50, # size of thread pool
  #     :message => message
  #   })
  #
  # To start sending, invoke Sender#start. The start method will block until
  # all addresses have been processed.
  class Sender
    # Initializes a new instance with the supplied options hash, and creates a
    # thread pool (with a default maximum size of 20).
    def initialize(opts)
      @opts = opts
      @pool = ThreadPool.new(@opts[:concurrent] || 20)
      @smtp_servers = {}
      @mutex = Mutex.new
    end
    
    # Starts processing the addresses by sending each one a message, and waits
    # for the thread pool to finish its work.
    def start
      @opts[:list].each {|a| @pool.process {send(a)}}
      @pool.join
    end
    
    # returns an array of SMTP servers for the email address. If an invalid 
    # address is specified, an error is raised. SMTP servers for each email
    # domain are memoized to improve performance.
    def smtp_servers_for_addr(addr)
      if (addr =~ /<(.*)@(.*)>/) or (addr =~ /(.*)@(.*)/)
        domain = $2
      else
        raise RuntimeError, "Invalid email address."
      end
      @mutex.synchronize do
        @smtp_servers[domain] ||= BulkMail.smtp_servers_for_domain(domain)
      end
    end
    
    # Sends an email to a recipient by trying any of the SMTP servers available
    # for the recipient.
    def send(addr)
      servers = smtp_servers_for_addr(addr)
      if servers.empty?
        report_addr(addr, :error, 'Unknown domain')
        return
      end
      servers.each do |server|
        begin
          send_message(server, addr)
          report_addr(addr, :success)
          return
        rescue => e
          report_addr(addr, :error, e.message)
        end
      end
    end
    
    # Opens a connection to an SMTP server and sends the message.
    def send_message(server, addr)
      Net::SMTP.start(
        server, nil, @opts[:helo], nil, nil, nil) do |smtp|
        smtp.send_message @opts[:message].to_s, @opts[:from], addr
      end
    end
    
    # Reports success or failure for the recipient.
    def report_addr(addr, status, error = nil)
      @mutex.synchronize do
        case status
        when :success:
          puts "Sent mail to #{addr}"
          File.open('sent.bulkmail', 'a') {|f| f.puts "#{addr}"}
        when :error:
          puts "Failed to send to #{addr}: #{error}"
          File.open('failed.bulkmail', 'a') {|f| f.puts "#{addr}: #{error}"}
        end
      end
    end
  end
end
